package com.rf.richfitwheel.admin.shiro;

import com.rf.richfitwheel.admin.conf.CommonPropertiesConfig;
import com.rf.richfitwheel.admin.conf.RediesProperties;
import com.rf.richfitwheel.admin.conf.SpringGlobalExceptionHandler;
import com.rf.richfitwheel.admin.shiro.credentials.RetryLimitHashedCredentialsMatcher;
import com.rf.richfitwheel.admin.shiro.filter.CustomFormAuthenticationFilter;
import com.rf.richfitwheel.admin.shiro.filter.KickoutSessionControlFilter;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.ShiroHttpSession;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.crazycake.shiro.*;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Autowired
    private RediesProperties properties;
    @Autowired
    private CommonPropertiesConfig commonPropertiesConfig;

    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意：单独一个ShiroFilterFactoryBean配置是或报错的，以为在
     * 初始化ShiroFilterFactoryBean的时候需要注入：SecurityManager
     * Filter Chain定义说明 1、一个URL可以配置多个Filter，使用逗号分隔 2、当设置多个过滤器时，全部验证通过，才视为通过
     * 3、部分过滤器可指定参数，如perms，roles
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new PluginShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        //自定义拦截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        filtersMap.put("kickout", kickoutSessionControlFilter());
        //shiro session失效处理。
        filtersMap.put("authc", formAuthenticationFilter(commonPropertiesConfig.getFeignAccessToken()));
        shiroFilterFactoryBean.setFilters(filtersMap);

        // 权限控制map.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/editor-app/**", "anon");
        filterChainDefinitionMap.put("/templates/**", "anon");
        filterChainDefinitionMap.put("/*.html", "anon");
        filterChainDefinitionMap.put("/*.js", "anon");
        filterChainDefinitionMap.put("/actuator/**", "anon");
        filterChainDefinitionMap.put("/app/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/sys/login/captcha.jpg", "anon");
        filterChainDefinitionMap.put("/sys/login/login", "anon");
        filterChainDefinitionMap.put("/sys/login/iamLogin", "anon");//统一身份认证登录
        filterChainDefinitionMap.put("/sys/login/ukeyCheckSign", "anon");//ukey验证接口
        filterChainDefinitionMap.put("/sys/iam/**", "anon"); //统一身份认证相关接口
        filterChainDefinitionMap.put("/sys/login/restSessionTime", "anon");
        filterChainDefinitionMap.put("/sys/org/treeListExcludeDeptByMainId", "anon");//获取机构树
        filterChainDefinitionMap.put("/kkfileview/**", "anon");
        filterChainDefinitionMap.put("/filemng/**", "anon");
        //财务公司接口
        filterChainDefinitionMap.put("/finderivatives/financeupwardapi/financeForwardDaily", "anon");
        //接口服务采用自己的接口验签
        filterChainDefinitionMap.put("/kdapi/**", "anon");
        //资金监控大屏
        filterChainDefinitionMap.put("/monitoring/bigScreen/**", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        //如果要限制同一账号登录人数用下面这个配置
        //filterChainDefinitionMap.put("/**", "kickout,authc");



        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(userRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * 身份认证realm; (这个需要自己写，账号密码校验；权限等)
     */
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCachingEnabled(true);
        //启用身份验证缓存，即缓存AuthenticationInfo信息，默认false
        //userRealm.setAuthenticationCachingEnabled(true);
        //缓存AuthenticationInfo信息的缓存名称
        //userRealm.setAuthenticationCacheName("authenticationCache");
        //启用授权缓存，即缓存AuthorizationInfo信息，默认false
        userRealm.setAuthorizationCachingEnabled(true);
        //缓存AuthorizationInfo信息的缓存名称 
        userRealm.setAuthorizationCacheName("authorizationCache");
        userRealm.setCredentialsMatcher(retryLimitHashedCredentialsMatcher());
        return userRealm;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     */
    public IRedisManager redisManager(){
        if(properties.getSentinel() != null && StringUtils.isNotBlank(properties.getSentinel().getMaster()) && !(properties.getSentinel().getNodes().isEmpty())){
            RedisSentinelManager redisManager = new RedisSentinelManager();
            redisManager.setMasterName(properties.getSentinel().getMaster());
            redisManager.setHost(String.join(",", properties.getSentinel().getNodes()));
            redisManager.setTimeout(properties.getTimeout());// redis连接超时时间
            redisManager.setPassword(properties.getPassword());
            redisManager.setDatabase(properties.getDatabase());
            return redisManager;
        }else{
            if(properties.getCluster() != null && !(properties.getCluster().getNodes().isEmpty())){
                RedisClusterManager redisManager = new RedisClusterManager();
                redisManager.setHost(String.join(",", properties.getCluster().getNodes()));
                redisManager.setTimeout(properties.getTimeout());// 配置缓存过期时间
                //需要注意，多个redis服务必须密码一致（巨坑）
                redisManager.setPassword(properties.getPassword());
                redisManager.setDatabase(properties.getDatabase());
                return redisManager;
            }else{
                RedisManager redisManager = new RedisManager();
                redisManager.setHost(properties.getHost() + ":" + properties.getPort());
                redisManager.setTimeout(properties.getTimeout());// redis连接超时时间
                redisManager.setPassword(properties.getPassword());
                redisManager.setDatabase(properties.getDatabase());
                return redisManager;
            }
        }
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setPrincipalIdFieldName("id");
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        ShiroSessionManager sessionManager = new ShiroSessionManager();
        Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
        cookie.setSameSite(Cookie.SameSiteOptions.LAX);
//		DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //设置session过期时间，默认就是30分钟
        sessionManager.setGlobalSessionTimeout(1800000L);
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * cookie对象;
     */
    public SimpleCookie rememberMeCookie() {
        //这个参数是cookie的名称，对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //<!-- 记住我cookie生效时间30天 ,单位秒;-->
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }

    /**
     * cookie管理对象;记住我功能
     */
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }

    public CustomFormAuthenticationFilter formAuthenticationFilter(String feignAccessToken) {
        CustomFormAuthenticationFilter formAuthenticationFilter = new CustomFormAuthenticationFilter(feignAccessToken);
        return formAuthenticationFilter;
    }

    /**
     * 限制同一账号登录同时登录人数控制
     *
     * @return
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter(){
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
    	//使用cacheManager获取相应的cache来缓存用户登录的会话；用于保存用户—会话之间的关系的；
    	//这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
    	//也可以重新另写一个，重新配置缓存时间之类的自定义缓存属性
    	kickoutSessionControlFilter.setCacheManager(cacheManager());
    	//用于根据会话ID，获取会话进行踢出操作的；
    	kickoutSessionControlFilter.setSessionManager(sessionManager());
    	//是否踢出后来登录的，默认是false；即后者登录的用户踢出前者登录的用户；踢出顺序。
    	kickoutSessionControlFilter.setKickoutAfter(false);
    	//同一个用户最大的会话数，默认1；比如2的意思是同一个用户允许最多同时两个人登录；
    	kickoutSessionControlFilter.setMaxSession(1);
    	//被踢出后重定向到的地址；
    	kickoutSessionControlFilter.setKickoutUrl("/kickout");
        return kickoutSessionControlFilter;
    }
    @Bean
    public RetryLimitHashedCredentialsMatcher retryLimitHashedCredentialsMatcher() {
        RetryLimitHashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher(cacheManager());
        credentialsMatcher.setHashAlgorithmName("md5");
        credentialsMatcher.setHashIterations(2);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**
     * 开启aop注解支持
     * 即在controller中使用 @RequiresPermissions("sys:user:info")
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        //设置安全管理器
        attributeSourceAdvisor.setSecurityManager(securityManager);
        return attributeSourceAdvisor;
    }

    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * 处理未授权的异常，返回自定义的错误（403）,不同于401这个代表的是资源没有权限而不是没登录
     *
     * @return
     */
    @Bean
    public SpringGlobalExceptionHandler springGlobalExceptionHandler() {
        return new SpringGlobalExceptionHandler();
    }
}
